6章 複雑な業務ロジックに立ち向かう
複雑な業務ロジックを扱う方法としてドメインモデルを、ドメインモデルを実装する部品として集約と値オブジェクトを学ぶ
不変性を持つ値オブジェクトや、自身の状態を変更を行える限られた公開インターフェースを持つ集約を活用することでシステムの自由度を制限することができる
自由度の低いシステムは複雑になりにくい
ドメインモデル
値オブジェクト
値オブジェクトは、オブジェクト内部のフィールド値の組み合わせによって識別されるオブジェクト
ex) 色を識別するオブジェクト
code:c++
class Color
{
int _red;
int _green;
int _blue:
}
値オブジェクトは事業活動を表現する基本部品として考える
プログラミング言語の標準搭載された、プリミティブ型のみを使って事業活動を表現するやり方は「基本データ方への執着」と呼ばれ嫌な匂いの元凶
code:ex.c++
class Person
{
private int _id;
private string _firstName;
private string _lastName;
private string _email;
private string _countryCode;
}
このクラスは誤った入力を弾くため、すべての入力値の妥当性を検証する必要がある
必然的に、このクラスのオブジェクトを生成するあらゆる場所に、妥当性を検証するロジックが散漫し重複が生まれる
また、値が検証済みであることを証明するのが難しい
値オブジェクトを使って実装すると表現したいことがまとまり、意図が明確になる
code:valueObject.c++
class Person
{
private PersonId _id;
private Name _name;
private EmailAddress _email;
private CountryCode _countryCode;
}
static void Main(string[] args)
{
var dave = new Person(
id: new PersonId(12345),
name: new Name("Dave", "Ancelovici"),
emal: Email.Parse("dave@example.com"),
country: CountryCode.Parse("BG")
);
}
値の妥当性を値オブジェクト内にカプセル化できる
値オブジェクトが業務の概念を表現することで、ソースコードでユビキタス言語を表現できる ex) 色を表現する業務ロジック
データ操作やインスタンス生成法などの業務ロジックをカプセル化した値オブジェクト
code:color.c++
var red = Color.FromRGB(255, 0, 0);
var green = Color.Green;
var yellow = red.MixWith(green);
var yellowString = yellow.ToString(): // "#FFFF00"
実装方法
値オブジェクトのフィールドを変更したい場合は、別のインスタンスを生成する
値オブジェクト同士が等しいかどうかは、フィールド値が同じかどうかで判定する
code:Color.c++
class Color
{
public bool Equals(object obj)
{
var other = obj as Color;
return other != null &&
this.Red == other.Red &&
this.Green == other.Green &&
this.Blue == other.Blue;
}
}
いつ使うか
使えるときはいつでも使う
イミュータブルなので、複数スレッドを同時並行で実行しても問題にならない
エンティティの属性として値オブジェクトを使う
エンティティ
エンティティは、どんな事業領域モデルでもドメインモデルの基本となる部品
個々のインスタンスを特定するための識別情報(ID)が必要
ex: 保険証番号、マイナンバーなど、個人を識別できる情報など
code:Person.cpp
class Person
{
public readonly PersonId Id;
public Name Name { get: set: }
public Person(PersonId id, Name name)
{
this.Id = id;
this.Name = name;
}
}
よほど特殊なケースを除き、エンティティを識別する値はその個体が存在する限り変更してはいけない
値オブジェクトとの違い
エンティティは不変ではない
値オブジェクトは、エンティティの状態を表現する手段
エンティティは必ず、集約の実装の一部になる
集約
集約は、一意に識別可能なフィールドをもつエンティティであり、データの一貫性を強制する境界
集約内部の業務ロジックだけが状態を変更できるようにすることでデータの一貫性を強制する
集約に記述するロジックは、状態を変更しようとする外部からのすべての操作の妥当性を検証し、業務ルールに違反した状態になることを防ぐ必要がある
並行処理時も同様に、一貫性が保てるような工夫が必要
集約にバージョン番号を持たせて、更新のたびにバージョンを増やすようにする
takumines.icon集約の境界設計をする際は、データの一貫性に注目する
集約が公開インターフェースとして外部に提供する状態変更メソッドを「コマンド」と呼ぶ
実装方法
code:c++
public class Ticket
{
public void AddMessage(UserId from, string body)
{
var message = new Message(from, body):
_message.Append(message):
}
}
// コマンドをパラメータオブジェクトとして表現するパターン
public class Ticket
{
public void Execute(AddMessage cmd)
{
var message = new Message(cmd.from, cmd.body):
_message.Append(message):
}
}
集約に業務ロジックを集めると、アプリケーション層がシンプルになる
集約オブジェクトの生成や、コマンド実行して状態を変更し、永続化するといった処理だけになる
トランザクション境界
集約の状態を変更する一連の処理は、単一のトランザクションとしてコミットする必要がある
なので、複数の集約にまたがるトランザクションを実行してはならない
上記を違反している場合は、集約の境界を見直した方が良い
集約の構造
業務ロジックがいくつかのエンティティと値オブジェクトを必要とするならば、関連するオブジェクト全体が1つの集約となる
あるエンティティを集約で保持するか否かは、集約に実装する業務ロジックに注目して検討する
結果的に整合性が取れていれば良い情報を扱う場合は、集約には含めない
集約が外部に公開するインターフェースをもつエンティティは1つにする(集約のルート)
集約の外からは集約のルートを通してのみアクセスする
集約間の参照はIDなどの識別子で行う
業務イベント
業務イベント(Domain Events)は、事業活動の中で起きた重要な出来事を表現するメッセージ
出来事を表現するため、クラス名は必ず過去形になる
また、事業活動で何が起きたか明確に表現できるような命名にする
集約は業務イベントを発行(publish)し、他のプロセス、集約、外部サービスがイベントを購読(subscribe)する